/**
 * Copyright 2015 jianglibo@gmail.com
 *
 */
package com.m3958.logcollector.whl;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.m3958.logcollector.domain.TreeBlob;
import com.m3958.logcollector.exception.tree.DuplicatedNodeException;
import com.m3958.logcollector.exception.tree.EmptyNameException;
import com.m3958.logcollector.repository.TreeBlobRepository;
import com.m3958.logcollector.service.NextSeqNumber;
import com.m3958.logcollector.tree.Tree;
import com.m3958.logcollector.tree.TreeNode;
import com.m3958.logcollector.tree.TreeUpdateAction;
import com.m3958.logcollector.tree.TreeUpdateAction.TreeUpdateActionResult;

/**
 * @author jianglibo@gmail.com
 *         2016年1月13日
 *
 */
public class TreeWhlAssistant<T> {

    private final ReentrantLock locker;

    private final String delimiter = "79482198FC5D41F8B554E7DCBCBD69FC";

    private final Path logfile;

    private PrintWriter writer;

    private Tree<T> tree;

    private final long treeId;

    private final TreeBlobRepository tbRepo;

    private final ObjectMapper objectMapper;

    private final NextSeqNumber nsn;
    
    private JavaType treeType;
    
    private JavaType tuaType;

    /**
     * @throws IOException
     * 
     */
    public TreeWhlAssistant(JavaType treeType, JavaType tuaType, ReentrantLock locker, NextSeqNumber nsn, TreeBlobRepository tbRepo, ObjectMapper objectMapper, Path logfile, long treeId)
            throws IOException {
        this.treeType = treeType;
        this.tuaType = tuaType;
        this.locker = locker;
        this.nsn = nsn;
        this.tbRepo = tbRepo;
        this.objectMapper = objectMapper;
        this.treeId = treeId;
        this.logfile = logfile;
    }

    /**
     * 
     * @param treeId
     * @return
     * @throws IOException
     */
    private void createPw() throws IOException {
        BufferedWriter bw = Files.newBufferedWriter(logfile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
        this.writer = new PrintWriter(bw, true);
    }

    /**
     * 假如由于断电或者关机，log目录中存在对应的文件。（几乎是必然）
     * 此时，db中的树的内容肯定不是最新的，必须将树加载到内存中，然后应用log中的改变，然后保存到数据库，然后删除log文件。
     * 
     * @throws JsonParseException
     * @throws JsonMappingException
     * @throws IOException
     */
    public void loadTree() throws JsonParseException, JsonMappingException, IOException {
        locker.lock();
        try {
            TreeBlob tb = tbRepo.findOne(treeId);
            tree = objectMapper.readValue(tb.getTreeContent(), treeType);
            processLogFile();
            tree.setJsonLength(tb.getTreeContent().length());
            createPw();
        } finally {
            locker.unlock();
        }
    }

    /**
     * 如果从cache中隐退时，其中的修改其实并未同步，只要将内存中的树保存到db，同时删除文件即可。
     * 
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    public void onRemoveFromCache() throws JsonParseException, JsonMappingException, IOException {
        locker.lock();
        try {
            String content = objectMapper.writeValueAsString(tree);
            TreeBlob tb = tbRepo.findOne(treeId);
            tb.setTreeContent(content);
            tbRepo.save(tb);
            // 这里有个空隙，如果此时断电，可能在下次加载的时候重新播放日志。已经添加的不会在添加，已经删除的不会再删除，修改的重复一下还是一样。
            // 所以应该没什么问题。
            if (writer != null) {
                writer.close();
            }
            Files.delete(logfile);
        } finally {
            locker.unlock();
        }
    }

    private void processLogFile() throws IOException {
        if (Files.exists(logfile)) {
            StringBuilder sb = new StringBuilder();
            Iterator<String> lineit = Files.readAllLines(logfile, StandardCharsets.UTF_8).iterator();
            String line;
            while (lineit.hasNext()) {
                line = lineit.next();
                if (delimiter.equals(line)) {
                    processOneItem(sb);
                    sb = new StringBuilder();
                } else {
                    sb.append(line);
                }
            }

            if (sb.length() > 0) {
                processOneItem(sb);
            }
        }
    }

    private void processOneItem(StringBuilder sb) {
        if (sb.length() < 1) {
            return;
        }
        try {
            TreeUpdateAction<T> tua = objectMapper.readValue(sb.toString(), tuaType);
            tua = doUpdate(tua, true);
        } catch (IOException e) {
        }
    }

    public TreeUpdateAction<T> doUpdate(TreeUpdateAction<T> tua) {
        return doUpdate(tua, false);
    }

    private TreeUpdateAction<T> doUpdate(TreeUpdateAction<T> tua, boolean replay) {
        locker.lock();
        try {
            TreeNode<T> tn;
            switch (tua.getAction()) {
            case ADD:
                long newId;
                if (replay) { // 重放日志
                    newId = tua.getGeneratedNodeId();
                    if (tree.findById(newId) != null) { // 重复重放日志。
                        return null;
                    }
                } else {
                    newId = nsn.getNextSeqId();
                }

                tn = new TreeNode<T>(newId, tua.getName());
                tn.setNodeValue(tua.getBody());
                tree.addChild(tua.getNodeId(), tn);
                tua.setGeneratedNodeId(tn.getId());
                break;
            case DELETE:
                tree.removeNode(tua.getNodeId());
                break;
            case EDIT:
                tn = tree.findById(tua.getNodeId());
                tn.setName(tua.getName());
                tn.setNodeValue(tua.getBody());
                break;
            default:
                break;
            }
            if (!replay) { // 保存日志
                write(tua);
            }
            return tua;
        } catch (JsonProcessingException e) {
            tua.setActionResult(TreeUpdateActionResult.JSON_ERROR);
        } catch (EmptyNameException e) {
            tua.setActionResult(TreeUpdateActionResult.EMPTY_NAME);
        } catch (DuplicatedNodeException e) {
            tua.setActionResult(TreeUpdateActionResult.DUPLICATED);
        } finally {
            locker.unlock();
        }
        return tua;
    }

    private void write(TreeUpdateAction<T> tua) throws JsonProcessingException {
        writer.println(objectMapper.writeValueAsString(tua));
        writer.println(delimiter);
    }

    public Path getTreelog() {
        return logfile;
    }

    public Tree<T> getTree() {
        return tree;
    }

    public void setTree(Tree<T> tree) {
        this.tree = tree;
    }
}
